<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\renderers;

/**
 * The <b>Renderer</b> class represents the <i>View</i> part of the MVC implementation in
 * the Switcher framework. It's role is to render the view using data present in the
 * context (MVC <i>Model</i> part). The context is initially filled by a <b>Controller</b>
 * (MVC <i>Controller</i> part) and can be altered during the rendering phase.
 * 
 * <p>This is the base class of all renderers. User-defined renderers must extend this class
 * or any subclass. It provides all the utility methods to manipulate the context. It also
 * declares a single endpoint for rendering.</p>
 *
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\renderers
 * @since 1.0.0
 */
abstract class Renderer
{
	/**
	 * If needed, a renderer can rely on a template. Even though it is optional, declared here
	 * it ensures that all renderers use the same property.
	 * @var mixed
	 */
	protected $template;
	
	/**
	 * Either a string or an array of string, compatible content types declaration.
	 * @var mixed
	 */
	protected $contentType;
	
	/**
	 * The <i>Model</i> part of the MVC implementation in the Switcher framework. Manipulations of
	 * the context shouldn't be done without variable name checking.
	 * @var array
	 * @see #checkContextVariableName
	 */
	protected $context = array();
	
	/**
	 * Logger.
	 * @var switcher\log\Logger
	 */
	protected $logger;
	
	/**
	 * No-arg constructor.
	 */
	public function __construct()
	{
	}
	
	/**
	 * This is the endpoint of all renderers. This method is called by the Switcher framework
	 * for each valid controllers. A renderer should throw a <b>RendererException</b> if a misuse
	 * is detected.
	 * 
	 * @see switcher\exceptions\RendererException
	 */
	public abstract function doRendering();
	
	/**
	 * Set the content type of the rendererd view. If several content types fit the view, the
	 * user agent's preferred content type. If no content type match the user agent's accepted
	 * content types, the first one is chosen by default.
	 * 
	 * @throws InvalidArgumentException If contentType is not a string or an array
	 */
	public final function setContentType()
	{
		if ( is_string($this->contentType) )
		{
			\switcher\utils\HttpHelper::setContentType($this->contentType);
		}
		elseif ( is_array($this->contentType) )
		{
			$this->negociateContentType();
		}
		else
		{
			throw new \InvalidArgumentException('Invalid content type : ' . gettype($this->contentType));
		}
	}
	
	/**
	 * Find the best match among available content types and set the response header.
	 */
	private function negociateContentType()
	{
		$acceptedContentTypes = \switcher\utils\HttpHelper::getAcceptedContentTypes();
		
		foreach ($acceptedContentTypes as $acceptedContentType)
		{
			$foundType = $this->findSupportedContentType($acceptedContentType);
			
			if ($foundType !== false)
			{
				\switcher\utils\HttpHelper::setContentType($foundType);
				return;
			}
		}
		
		\switcher\utils\HttpHelper::setContentType($this->contentType[0]);
	}
	
	/**
	 * Find the best match for a specific content type accepted by the user agent.
	 * 
	 * @param string $acceptedContentType The content type to match
	 * @return mixed The best match or false
	 */
	private function findSupportedContentType($acceptedContentType)
	{
		$acceptedParts = preg_split('/\s*;\s*/', $acceptedContentType);
		$acceptedMime = $acceptedParts[0];
		
		foreach ($this->contentType as $supportedContentType)
		{
			$supportedParts = preg_split('/\s*;\s*/', $supportedContentType);
			$supportedMime = $supportedParts[0];
			
			if ( fnmatch($acceptedMime, $supportedMime) )
			{
				return $this->negociateCharset($supportedParts);
			}
		}
		
		return false;
	}
	
	/**
	 * Find the best match for a specific charset type accepted by the user agent.
	 * 
	 * @param array $supportedParts A splitted supported content type
	 * @return string The content type and the charset if compatible
	 */
	private function negociateCharset(array $supportedParts)
	{
		if (count($supportedParts) == 2)
		{
			$charset = substr($supportedParts[1], strlen('charset='));
			$acceptedCharsets = \switcher\utils\HttpHelper::getAcceptedCharsets();
			
			foreach ($acceptedCharsets as $acceptedCharset)
			{
				if (strcasecmp($acceptedCharset, $charset) == 0)
				{
					return $supportedParts[0] . '; charset=' . $charset;
				}
			}
		}
		
		return $supportedParts[0];
	}
	
	public function setTemplate($template)
	{
		$this->template = $template;
	}
	
	/**
	 * Get a context parameter.
	 * 
	 * @param string $name The name of the context parameter
	 * @return mixed The context parameter or null if it doesn't exist
	 * @throws RendererException If $name isn't a valid context parameter name
	 */
	public final function getContextParameter($name)
	{
		$this->checkContextVariableName($name);
		return isset( $this->context[$name] ) ? $this->context[$name] : null;
	}
	
	/**
	 * Set a context parameter.
	 * 
	 * @param string $name The name of the context parameter
	 * @throws RendererException If $name isn't a valid context parameter name
	 */
	public final function setContextParameter($name, $value)
	{
		$this->checkContextVariableName($name);
		$this->context[$name] = $value;
	}
	
	/**
	 * Set context parameters.
	 * 
	 * @param array $values An associative array containing named values
	 * @throws RendererException If a value doesn't have a valid context parameter name
	 */
	public final function setContextParameters(array $values)
	{
		foreach($values as $name=>$value)
		{
			$this->setContextParameter($name, $value);
		}
	}
	
	/**
	 * Remove a context parameter.
	 * 
	 * @param string $name The name of the context parameter
	 * @throws RendererException If $name isn't a valid context parameter name
	 */
	public final function removeContextParameter($name)
	{
		$this->checkContextVariableName($name);
		
		if ( isset( $this->context[$name] ) )
		{
			unset( $this->context[$name] );
		}
	}
	
	/**
	 * Check if a context variable name matches the following regexp : <b>/\w+/</b>
	 * 
	 * @param string $name A name for a context parameter
	 * @throws RendererException If $name isn't a valid context parameter name
	 */
	protected final function checkContextVariableName($name)
	{
		if (preg_match('/\w+/', $name) != 1)
		{
			throw new \switcher\exceptions\RendererException('Invalid context variable name : ' . $name);
		}
	}
}

?>